package checks

import (
	"cmp"
	"context"
	"fmt"
	"slices"
	"strconv"
	"strings"

	"github.com/cloudflare/pint/internal/diags"
	"github.com/cloudflare/pint/internal/discovery"
	"github.com/cloudflare/pint/internal/parser/utils"

	"github.com/prometheus/prometheus/model/labels"
)

const (
	RuleDependencyCheckName = "rule/dependency"
)

func NewRuleDependencyCheck() RuleDependencyCheck {
	return RuleDependencyCheck{}
}

type RuleDependencyCheck struct{}

func (c RuleDependencyCheck) Meta() CheckMeta {
	return CheckMeta{
		States: []discovery.ChangeType{
			discovery.Removed,
		},
		Online:        false,
		AlwaysEnabled: false,
	}
}

func (c RuleDependencyCheck) String() string {
	return RuleDependencyCheckName
}

func (c RuleDependencyCheck) Reporter() string {
	return RuleDependencyCheckName
}

func (c RuleDependencyCheck) Check(_ context.Context, entry *discovery.Entry, entries []*discovery.Entry) (problems []Problem) {
	if entry.Path.Name != entry.Path.SymlinkTarget {
		// Don't reported symlinks that are being removed.
		return problems
	}

	filtered := nonRemovedEntries(entries)

	for _, flt := range filtered {
		if flt.Rule.Type() == entry.Rule.Type() && flt.Rule.Name() == entry.Rule.Name() {
			// There's another rule with same type & name, do nothing.
			return problems
		}
	}

	var broken []*brokenDependency
	var dep *brokenDependency
	for _, flt := range filtered {
		if entry.Rule.RecordingRule != nil {
			dep = c.usesVector(flt, entry.Rule.RecordingRule.Record.Value)
		}
		if entry.Rule.AlertingRule != nil {
			dep = c.usesAlert(flt, entry.Rule.AlertingRule.Alert.Value)
		}
		if dep != nil {
			var found bool
			for _, b := range broken {
				if b.kind == dep.kind && b.path == dep.path && b.line == dep.line && b.name == dep.name {
					found = true
					break
				}
			}
			if !found {
				broken = append(broken, dep)
			}
		}
	}

	if len(broken) == 0 {
		return problems
	}

	slices.SortStableFunc(broken, func(a, b *brokenDependency) int {
		return cmp.Or(
			cmp.Compare(a.path, b.path),
			cmp.Compare(a.line, b.line),
			cmp.Compare(a.name, b.name),
		)
	})

	var details strings.Builder
	details.WriteString("If you remove the ")
	details.WriteString(broken[0].kind)
	details.WriteString(" rule generating `")
	details.WriteString(broken[0].metric)
	details.WriteString("`, and there is no other source of this metric, then any other rule depending on it will break.\n")
	details.WriteString("List of found rules that are using `")
	details.WriteString(broken[0].metric)
	details.WriteString("`:\n\n")
	for _, b := range broken {
		details.WriteString("- `")
		details.WriteString(b.name)
		details.WriteString("` at `")
		details.WriteString(b.path)
		details.WriteRune(':')
		details.WriteString(strconv.Itoa(b.line))
		details.WriteString("`\n")
	}

	name := entry.Rule.NameNode()
	problems = append(problems, Problem{
		Anchor:   AnchorBefore,
		Lines:    name.Pos.Lines(),
		Reporter: c.Reporter(),
		Summary:  "rule results used by another rule",
		Details:  details.String(),
		Severity: Warning,
		Diagnostics: []diags.Diagnostic{
			{
				Message:     fmt.Sprintf("Metric generated by this rule is used by %d other rule(s).", len(broken)),
				Pos:         name.Pos,
				FirstColumn: 1,
				LastColumn:  len(name.Value),
				Kind:        diags.Issue,
			},
		},
	})

	return problems
}

func (c RuleDependencyCheck) usesVector(entry *discovery.Entry, name string) *brokenDependency {
	expr := entry.Rule.Expr()
	if expr.SyntaxError() != nil {
		return nil
	}

	for _, vs := range utils.HasVectorSelector(expr.Query()) {
		if vs.Name == name {
			return &brokenDependency{
				kind:   "recording",
				metric: name,
				path:   entry.Path.SymlinkTarget,
				line:   expr.Value.Pos.Lines().First,
				name:   entry.Rule.Name(),
			}
		}
	}

	return nil
}

func (c RuleDependencyCheck) usesAlert(entry *discovery.Entry, name string) *brokenDependency {
	expr := entry.Rule.Expr()
	if expr.SyntaxError() != nil {
		return nil
	}

	for _, vs := range utils.HasVectorSelector(expr.Query()) {
		if vs.Name != "ALERTS" && vs.Name != "ALERTS_FOR_STATE" {
			continue
		}
		for _, lm := range vs.LabelMatchers {
			if lm.Name == "alertname" && lm.Type == labels.MatchEqual && lm.Value == name {
				return &brokenDependency{
					kind:   "alerting",
					metric: vs.String(),
					path:   entry.Path.SymlinkTarget,
					line:   expr.Value.Pos.Lines().First,
					name:   entry.Rule.Name(),
				}
			}
		}
	}

	return nil
}

type brokenDependency struct {
	kind   string
	metric string
	path   string
	name   string
	line   int
}

func nonRemovedEntries(src []*discovery.Entry) (dst []*discovery.Entry) {
	for _, entry := range src {
		if entry.State == discovery.Removed {
			continue
		}
		if entry.PathError != nil {
			continue
		}
		if entry.Rule.Error.Err != nil {
			continue
		}
		dst = append(dst, entry)
	}
	return dst
}
